/*
 * Copyright (c) 2010, Servoy-stuff
 * 
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 * 
 * This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
 * http://www.fsf.org/licensing/licenses/lgpl.txt
 */
package net.stuff.servoy.eclipse.plugin.utils;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;


import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextAction;
import org.mozilla.javascript.EvaluatorException;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.Kit;
import org.mozilla.javascript.RhinoException;
import org.mozilla.javascript.Script;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.tools.ToolErrorReporter;
import org.mozilla.javascript.tools.shell.QuitAction;

/**
 *
 * @author Servoy Stuff
 * http://www.servoy-stuff.net/
 *
 */
public class Main extends org.mozilla.javascript.tools.shell.Main {
	
	private static final String LF = System.getProperty("line.separator");
	private static final int EXITCODE_RUNTIME_ERROR = 3;
	private static final int EXITCODE_FILE_NOT_FOUND = 4;
	
	static List<String> myFileList;
	static boolean myProcessStdin = true;
	static boolean myInitialized;
	public static InputStream inputStream = null;
	public static Writer outputWriter = null;
	public static OutputStream outputStream = null;
	public static Reader inputReader = null;
	public static boolean stop;
	
	private static class MyProxy implements ContextAction, QuitAction
	{
		private static final int PROCESS_FILES = 1;
		private static final int EVAL_INLINE_SCRIPT = 2;
		private static final int SYSTEM_EXIT = 3;
		
		private int type;
		String[] args;
		String scriptText;
		
		MyProxy(int type)
		{
			this.type = type;
		}
		
		public Object run(Context cx)
		{
			if (type == PROCESS_FILES) {
				processFiles(cx, args);
			} else if (type == EVAL_INLINE_SCRIPT) {
				Script script = loadScriptFromSource(cx, scriptText,
						"<command>", 1, null);
				if (script != null) {
					evaluateScript(script, cx, getGlobal());
				}
			} else {
				throw Kit.codeBug();
			}
			return null;
		}
		public void quit(Context cx, int exitCode)
		{
			if (type == SYSTEM_EXIT) {
				//System.exit(exitCode);
				return;
			}
			throw Kit.codeBug();
		}
	}
	
	public static int exec(String origArgs[]) {
		stop = false;
		errorReporter = new ToolErrorReporter(false, global.getErr());
		shellContextFactory.setErrorReporter(errorReporter);
		String[] args = processOptions(origArgs);
		myFileList = new ArrayList<String>();
		processMyOptions(origArgs);
		if (myProcessStdin)
			myFileList.add(null);
		
		if (!myInitialized) {
			global.init(shellContextFactory);
			global.defineFunctionProperties(new String[] {"quit", "saveFile", "copyFile", "readSource"}, Main.class,
					ScriptableObject.DONTENUM);
			myInitialized = true;
		}
		MyProxy iproxy = new MyProxy(MyProxy.PROCESS_FILES);
		iproxy.args = args;
		shellContextFactory.call(iproxy);
		
		return exitCode;
	}
	
	public static void stop() {
		stop = true;
	}
	
	public static void quit(Context cx, Scriptable thisObj,
			Object[] args, Function funObj)
	{
		// do nothing!
	}
	
	/**
	 * @param args
	 */
	private static void processMyOptions(String[] args) {
		for (int i = 0; ; ++i) {
			if (i == args.length) {
				return;
			}
			String arg = args[i];
			if (!arg.startsWith("-")) {
				myProcessStdin = false;
				myFileList.add(arg);
				String[] result = new String[args.length - i - 1];
				System.arraycopy(args, i+1, result, 0, args.length - i - 1);
				return;
			}
		}
	}
	
	private static void processFiles(Context cx, String[] args)
	{
		// define "arguments" array in the top-level object:
		// need to allocate new array since newArray requires instances
		// of exactly Object[], not ObjectSubclass[]
		Object[] array = new Object[args.length];
		System.arraycopy(args, 0, array, 0, args.length);
		Scriptable argsObj = cx.newArray(global, array);
		global.defineProperty("arguments", argsObj,
				ScriptableObject.DONTENUM);
		
		for (String file: myFileList) {
			if (stop) break;
			inputStream = null;
			processFileSecure(cx, global, file, null);
		}
	}
		
	private static void processFileSecure(Context cx, Scriptable scope,
			String path, Object securityDomain)
	{
		if (stop) return;
		Script script ;
		String source = (String)readFileOrUrl(path, true);
		if (source == null) {
			exitCode = EXITCODE_FILE_NOT_FOUND;
			return;
		}
		
		// Support the executable script #! syntax:  If
		// the first line begins with a '#', treat the whole
		// line as a comment.
		if (source.length() > 0 && source.charAt(0) == '#') {
			for (int i = 1; i != source.length(); ++i) {
				int c = source.charAt(i);
				if (c == '\n' || c == '\r') {
					source = source.substring(i);
					break;
				}
			}
		}
		script = loadScriptFromSource(cx, source, path, 1, securityDomain);
		
		if (script != null) {
			evaluateScript(script, cx, scope);
		}
	}
	/**
	 * Read file or url specified by <tt>path</tt>.
	 * @return file or url content as <tt>byte[]</tt> or as <tt>String</tt> if
	 * <tt>convertToString</tt> is true.
	 */
	private static Object readFileOrUrl(String path, boolean convertToString)
	{
		if (stop) return null;
		try {
			return SourceReader.readFileOrUrl(path, convertToString, 
					null);
		} catch (IOException ex) {
			Context.reportError(ToolErrorReporter.getMessage(
					"msg.couldnt.read.source", path, ex.getMessage()));
			return null;
		}
	}
	
	public static String readSource(String path, String encoding) {
		File f = new File(path);
		try {
			final FileReader fr = new FileReader(f);
			inputReader = new BufferedReader(fr);
			
			String inLine = null;
			final StringBuffer buffer = new StringBuffer();
			int i = 1;
			while ((inLine = ((BufferedReader)inputReader).readLine()) != null) {
				if ("globals.js".equalsIgnoreCase(f.getName())) {
					buffer.append(inLine);
					buffer.append(LF);
				} else {
					int cnt = 4;
					if (f.getName().endsWith("_calculations.js")) cnt++;
					if (i > cnt) {
						buffer.append(inLine);
						buffer.append(LF);
					}
				}
				i++;
			}
			return buffer.substring(0, buffer.length()-(LF.length()+1));
		} catch (IOException ex) {
			// ignore
		} finally {
			try {
				if (inputReader != null) inputReader.close();
			} catch (IOException ex) {}
			inputReader = null;
		}
		return null;
	}
	
	public static void saveFile(String outDir, String fileName, String content, String encoding) {
		if (stop) return;
		try {
			outputWriter = new PrintWriter(
					new OutputStreamWriter(
							new FileOutputStream(outDir+"/"+fileName),
							encoding
					)
			);
			outputWriter.write(content);
			outputWriter.flush();
		} catch (IOException ex) {
			// ignore
		} finally {
			try {
			if (outputWriter != null) outputWriter.close();
			} catch (IOException ex) {}
			outputWriter = null;
		}
	}
	
	public static void copyFile(String in, String outDir, String fileName, boolean dummy) {
		if (stop) return;
		File inFile = new File(in);
		if (fileName == null || fileName.equalsIgnoreCase("undefined")) fileName = inFile.getName();
		File outFile = new File(outDir+"/"+fileName);
		//System.out.println(in + " -> "+outDir+"/"+fileName);
		try {
			inputStream = new FileInputStream(inFile);
			outputStream = new FileOutputStream(outFile);
			byte[] buf = new byte[1024];
			int len;
			while ((len = inputStream.read(buf)) > 0) {
				outputStream.write(buf, 0, len);
			}
			outputStream.flush();
		} catch (IOException ex) {
			// ignore
		} finally {
			try {
			if (outputStream != null) outputStream.close();
			} catch (IOException ex) {}
			outputStream = null;
			try {
				if (inputStream != null) inputStream.close();
			} catch (IOException ex) {}
			inputStream = null;
		}
	}
	
	
	public static Script loadScriptFromSource(Context cx, String scriptSource,
			String path, int lineno,
			Object securityDomain)
	{
		if (stop) return null;
		try {
			return cx.compileString(scriptSource, path, lineno,
					securityDomain);
		} catch (EvaluatorException ee) {
			// Already printed message.
			exitCode = EXITCODE_RUNTIME_ERROR;
		} catch (RhinoException rex) {
			ToolErrorReporter.reportException(
					cx.getErrorReporter(), rex);
			exitCode = EXITCODE_RUNTIME_ERROR;
		} catch (VirtualMachineError ex) {
			// Treat StackOverflow and OutOfMemory as runtime errors
			ex.printStackTrace();
			String msg = ToolErrorReporter.getMessage(
					"msg.uncaughtJSException", ex.toString());
			exitCode = EXITCODE_RUNTIME_ERROR;
			Context.reportError(msg);
		}
		return null;
	}
	
	public static class SourceReader
	{
		public static Object readFileOrUrl(String path, boolean convertToString, 
				String defaultEncoding) throws IOException
		{
			if (stop) return null;
			URL url = null;
			// Assume path is URL if it contains dot and there are at least
			// 2 characters in the protocol part. The later allows under Windows
			// to interpret paths with driver letter as file, not URL.
			if (path.indexOf(':') >= 2) {
				try {
					url = new URL(path);
				} catch (MalformedURLException ex) {
				}
			}
			
			inputStream = null;
			int capacityHint = 0;
			String encoding = null;
			final String contentType;
			byte[] data;
			try {
				contentType = encoding = null;
				if (url == null) {
					File file = new File(path);
					capacityHint = (int)file.length();
					inputStream = new FileInputStream(file);
				}
				if (capacityHint <= 0) {
					capacityHint = 4096;
				}
				if (stop) return null;
				data = Kit.readStream(inputStream, capacityHint);
			} finally {
				if(inputStream != null) {
					inputStream.close();
				}
			}
			
			Object result;
			if (!convertToString) {
				result = data;
			} else {
				if(encoding == null) {
					// None explicitly specified in Content-type header. Use RFC-4329
					// 4.2.2 section to autodetect
					if(data.length > 3 && data[0] == -1 && data[1] == -2 && data[2] == 0 && data[3] == 0) {
						encoding = "UTF-32LE";
					}
					else if(data.length > 3 && data[0] == 0 && data[1] == 0 && data[2] == -2 && data[3] == -1) {
						encoding = "UTF-32BE";
					}
					else if(data.length > 2 && data[0] == -17 && data[1] == -69 && data[2] == -65) {
						encoding = "UTF-8";
					}
					else if(data.length > 1 && data[0] == -1 && data[1] == -2) {
						encoding = "UTF-16LE";
					}
					else if(data.length > 1 && data[0] == -2 && data[1] == -1) {
						encoding = "UTF-16BE";
					}
					else {
						// No autodetect. See if we have explicit value on command line
						encoding = defaultEncoding;
						if(encoding == null) {
							// No explicit encoding specification
							if(url == null) {
								// Local files default to system encoding
								encoding = System.getProperty("file.encoding");
							}
							else if(contentType != null && contentType.startsWith("application/")) {
								// application/* types default to UTF-8
								encoding = "UTF-8";
							}
							else {
								// text/* MIME types default to US-ASCII
								encoding = "US-ASCII";
							}
						}
					}
				}
				String strResult = new String(data, encoding);
				// Skip BOM
				if(strResult.length() > 0 && strResult.charAt(0) == '\uFEFF')
				{
					strResult = strResult.substring(1);
				}
				result = strResult;
			}
			return result;
		}
	}
	
	
}
